﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenTK.Graphics.OpenGL;
namespace Foundry.Graphics
{

    public abstract class Shader : IHandle, IDisposable
    {
        public bool Disposed { get; private set; }
        public int Handle { get; private set; }
        public ShaderType Type { get; private set; }
        public event EventHandler Disposing;
		
		private string source;
		
        public string Source { get { return source; } private set { source = value; GL.ShaderSource(Handle, source); } }
        protected Shader(ShaderType type)
        {
            OpenTK.Graphics.GraphicsContext.Assert();
            Type = type;
            Handle = GL.CreateShader(type);
        }
        public void MakeCurrent()
        {
            throw new NotSupportedException();
        }
        public void MakeNullCurrent()
        {
            throw new NotSupportedException();
        }
        public string InfoLog { get { return GL.GetShaderInfoLog(Handle); } }
        public IEnumerable<CompileLogEntry> CompileLog
        {
            get
            {
                var list = new List<CompileLogEntry>();
                string val = InfoLog;
                var lines = val.Split('\n');
                foreach (var line in lines)
                {
                    CompileLogEntry entry;
                    if (CompileLogEntry.TryParse(line, out entry))
                    {
                        list.Add(entry);
                    }
                }
                return list;
            }
        }
        public void Dispose()
        {
            if (Disposing != null)
                Disposing(this, EventArgs.Empty);

            GL.DeleteShader(Handle);
            Handle = 0;
            Disposed = true;
        }

        public void Compile()
        {
            GL.CompileShader(Handle);
        }

		public static T FromSource<T>(System.IO.Stream source)
			where T : Shader, new()
		{
			var new_shader = new T();
			System.IO.StreamReader rd= new System.IO.StreamReader(source);
			new_shader.Source = rd.ReadToEnd();
			return new_shader;
		}
		public static T FromSource<T>(System.IO.Stream source, Encoding enc)
			where T : Shader, new()
		{
			var new_shader = new T();
			System.IO.StreamReader rd= new System.IO.StreamReader(source);
			new_shader.Source = rd.ReadToEnd();
			return new_shader;
		}
		public static T FromSource<T>(string source)
			where T : Shader, new()
		{
			var new_shader = new T();
			new_shader.Source = source;
			return new_shader;
		}
		public static T FromFile<T>(string path)
			where T : Shader, new()
		{
			using(var f = System.IO.File.OpenRead(path))
			{
				return FromSource<T>(f);
			}
		}
		public static T FromFile<T>(string path, Encoding enc)
			where T : Shader, new()
		{
			using(var f = System.IO.File.OpenRead(path))
			{
				return FromSource<T>(f, enc);
			}
			
		}


    }
    

    public sealed class VertexShader : Shader
    {
        public VertexShader()
            : base(ShaderType.VertexShader)
        {
			
        }
		

		
    }

    public sealed class FragmentShader : Shader
    {
        public FragmentShader()
            : base(ShaderType.FragmentShader)
        {
        }
    }
    public sealed class GeometryShader : Shader
    {
        public GeometryShader()
            : base(ShaderType.GeometryShader)
        {
        }

    }

    public interface IShaderProgram : IHandle, IDisposable
    {
        VertexDescriptor VertexDescriptor { get; }
        
    }

    public class ShaderProgram : IShaderProgram
    {
        public int Handle { get; private set; }
        public event EventHandler Disposing;
        public bool Disposed { get; private set; }
        public static ShaderProgram Current { get; private set; }

        public VertexDescriptor VertexDescriptor { get; private set; }

        List<Shader> AttachedShaders;

        public System.Collections.ObjectModel.ReadOnlyCollection<Shader> Shaders { get { return AttachedShaders.AsReadOnly(); } }

        public DrawElementsType GeometryInputType
        {
            get
            {
                int value = 0;
                GL.GetProgram(Handle, ProgramParameter.GeometryInputType, out value);
                return (DrawElementsType)value;
            }
            set
            {
                GL.ProgramParameter(Handle, Version32.GeometryInputType, (int)value);
            }
        }

        public DrawElementsType GeometryOutputType
        {
            get
            {
                int value = 0;
                GL.GetProgram(Handle, ProgramParameter.GeometryOutputType, out value);
                return (DrawElementsType)value;
            }
            set
            {
                GL.ProgramParameter(Handle, Version32.GeometryOutputType, (int)value);
            }
        }

        public int GeometryMaxVertices
        {
            get
            {
                int value = 0;
                GL.GetProgram(Handle, ProgramParameter.GeometryVerticesOut, out value);
                return value;
            }
            set
            {
                GL.ProgramParameter(Handle, Version32.GeometryVerticesOut, value);
            }
        }
        public ShaderProgram(VertexDescriptor desc)
            : this()
        {
            VertexDescriptor = desc;
        }

        public ShaderProgram()
        {
            OpenTK.Graphics.GraphicsContext.Assert();
            
            AttachedShaders = new List<Shader>();
            Handle = GL.CreateProgram();
        }

        protected virtual void OnShaderCompiled()
        {
            
        }

        public bool Linked
        {
            get
            {
                int link_status = 0;
                GL.GetProgram(Handle, ProgramParameter.LinkStatus, out link_status);
                return link_status != 0;
            }
        }

        public int ActiveUniforms 
        {
            get
            {
                int uniforms = 0;
                GL.GetProgram(Handle, ProgramParameter.ActiveUniforms, out uniforms);
                return uniforms;
            }
        }
        public IEnumerable<string> Uniforms
        {
            get
            {
                string[] names = new string[64];
                int[] indices = new int[64];
                GL.GetUniformIndices(Handle, names.Length, names, indices);
                return names;
            }
        }

        public IUniform<T> GetUniform<T>(string name)
        {
            if (Disposed)
                throw new ObjectDisposedException(GetType().FullName);

            if (typeof(T) == typeof(float))
                return new ScalarFloatUniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(int))
                return new ScalarIntUniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(OpenTK.Vector2))
                return new Vector2Uniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(OpenTK.Vector3))
                return new Vector3Uniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(OpenTK.Vector4))
                return new Vector4Uniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(OpenTK.Graphics.Color4))
                return new Color4Uniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(OpenTK.Quaternion))
                return new QuaternionUniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(OpenTK.Matrix4))
                return new MatrixUniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(Texture1D))
                return new Texture1DUniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(Texture2D))
                return new Texture2DUniform(this, name) as IUniform<T>;
            else if (typeof(T) == typeof(Texture3D))
                return new Texture3DUniform(this, name) as IUniform<T>;

            else throw new NotSupportedException("Unsupported datatype in uniform");
        }

        ~ShaderProgram()
        {
            Dispose();
        }

        public void Dispose()
        {
            if (!Disposed && OpenTK.Graphics.GraphicsContext.CurrentContext != null)
			{
	            GC.SuppressFinalize(this);
	            if (Disposing != null)
	                Disposing(this, EventArgs.Empty);
	
	            GL.DeleteProgram(Handle);
	            Handle = 0;
	            Disposed = true;
			}
        }

        public void AttachShader(Shader shader)
        {
            if (Disposed)
                throw new ObjectDisposedException(GetType().FullName);
            if (!AttachedShaders.Contains(shader))
            {
                GL.AttachShader(Handle, shader.Handle);
                AttachedShaders.Add(shader);
            }
            else
                throw new Exception("Shader is already attached.");
        }
        public void DetachShader(Shader shader)
        {
            if (AttachedShaders.Contains(shader))
                GL.DetachShader(Handle, shader.Handle);
            else
                throw new IndexOutOfRangeException("Shader is not attached to this program.");
        }

        public void CompileAndLink()
        {
            if (Disposed)
                throw new ObjectDisposedException(GetType().FullName);

            GL.BindFragDataLocation(Handle, 0, "ColorOut");

            foreach (var shader in AttachedShaders)
			{
				int pars = 0;
                shader.Compile();
				GL.GetShader(shader.Handle, ShaderParameter.CompileStatus, out pars);
				if(pars == 0)
					throw new ShaderException(shader.InfoLog);
			}
            GL.ValidateProgram(Handle);
            int validate_status;
			
			
			
            if (VertexDescriptor != null)
            {
                for (int i = 0; i < VertexDescriptor.Items.Count; i++)
                {
                    GL.BindAttribLocation(Handle, i, VertexDescriptor.Items[i].Name);
                }
            }
            

            GL.LinkProgram(Handle);
			
			GL.GetProgram(Handle, ProgramParameter.LinkStatus, out validate_status);
			if(validate_status == 0)
				throw new ShaderException(InfoLog);
				
			              
			
            GL.GetProgram(Handle, ProgramParameter.ValidateStatus, out validate_status);
            if (validate_status == 0)
            {
                throw new ShaderCompileException("Shader comilation unsuccessfull:\n" + InfoLog);
            }


            OnShaderCompiled();
            
            //GL.BindFragDataLocation(Handle, 1, "NormalOut");
            //GL.BindFragDataLocation(Handle, 2, "DepthOut");
        }

        public string InfoLog { get { return GL.GetProgramInfoLog(Handle); } }

        public void MakeCurrent()
        {

            if (Disposed)
                throw new ObjectDisposedException(GetType().FullName);
            Current = this;
            GL.UseProgram(Handle);
        }
        public void MakeNullCurrent()
        {
            Current = null;
            GL.UseProgram(0);
        }
        

    }
    public enum Severity
    {
        Info,
        Warning,
        Error
    }

    public class CompileLogEntry
    {
        public int Line { get; private set; }
        public string ErrorCode { get; private set; }
        public string Message { get; private set; }
        public string Filename { get; private set; }
        public Severity Severity { get; private set; }
        static readonly System.Text.RegularExpressions.Regex reg = new System.Text.RegularExpressions.Regex(
            @"(?<Character>[0-9]+)\((?<LineNumber>[0-9]+)\)\s*:\s*(<?Severity>warning|error|info)\s+(?<ErrorCode>[a-fA-F0-9]+)\s*:\s*(?<Message>.*)",
            System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.Compiled);

        public static bool TryParse(string message, out CompileLogEntry value)
        {
            //0(3) : warning C7532: global type mat4 requires "#version 100" or later
            var nw = new CompileLogEntry();
            var m = reg.Match(message);
            if (m.Success)
            {
                nw.ErrorCode = m.Groups["ErrorCode"].Value;
                nw.Line = int.Parse(m.Groups["LineNumber"].Value);
                nw.Severity = (Severity)Enum.Parse(typeof(Severity), m.Groups["Severity"].Value, true);
                nw.Message = m.Groups["Message"].Value;
            }

            value = null;
            return false;
        }
    }
    public class ShaderException : Exception
    {
        public ShaderException(string message)
            : base(message)
        {
        }
    }
    public class ShaderCompileException : ShaderException
    {
        public ShaderCompileException(string message)
            : base(message)
        {
            
        }

    }
}
